package waypointquery

import (
	"errors"
	"fmt"
	"strconv"
	"strings"

	envoycorev3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
	networkingv1beta1 "istio.io/api/networking/v1beta1"
	networkingclient "istio.io/client-go/pkg/apis/networking/v1"
	istioutil "istio.io/istio/pilot/pkg/networking/util"
	"istio.io/istio/pilot/pkg/serviceregistry/provider"
	"istio.io/istio/pkg/slices"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"sigs.k8s.io/controller-runtime/pkg/client"

	"github.com/kgateway-dev/kgateway/v2/internal/kgateway/extensions2/plugins/kubernetes"
	"github.com/kgateway-dev/kgateway/v2/internal/kgateway/extensions2/plugins/serviceentry"
	"github.com/kgateway-dev/kgateway/v2/internal/kgateway/wellknown"
	"github.com/kgateway-dev/kgateway/v2/pkg/pluginsdk/ir"
	"github.com/kgateway-dev/kgateway/v2/pkg/utils/kubeutils"
	"github.com/kgateway-dev/kgateway/v2/pkg/utils/stringutils"
)

// ErrUnsupportedServiceType should never occur due to unexpected input.
// If we see this, there is a bug and we're converting a non-Service type into Service.
var ErrUnsupportedServiceType = errors.New("unsupported service type")

// Service is a common type to use between Service and ServiceEntry
type Service struct {
	client.Object
	GroupKind schema.GroupKind
	Aliases   []ir.ObjectSource
	Addresses []string
	Ports     []ServicePort
	Hostnames []string
}

// Keys give all the name/namespace/group/kind keys
// including those from aliases.
// Specifically returns the aliases _first_.
func (s Service) Keys() []ir.ObjectSource {
	aliases := s.Aliases

	// Check if the first alias is a ServiceEntry's own ObjectSource
	if len(aliases) > 0 && aliases[0].Equals(ir.ObjectSource{
		Name:      s.GetName(),
		Namespace: s.GetNamespace(),
		Group:     wellknown.ServiceEntryGVK.Group,
		Kind:      wellknown.ServiceEntryGVK.Kind,
	}) {
		// ServiceEntry has self as the first one (see BuildServiceEntryBackendObjectIR).
		// We want to return the aliases _after_ the self.
		aliases = aliases[1:]
	}
	return append(aliases, ir.ObjectSource{
		Name:      s.GetName(),
		Namespace: s.GetNamespace(),
		Group:     s.GroupKind.Group,
		Kind:      s.GroupKind.Kind,
	})
}

func (s Service) IsHeadless() bool {
	switch o := s.Object.(type) {
	case *corev1.Service:
		return o.Spec.ClusterIP == corev1.ClusterIPNone
	case *networkingclient.ServiceEntry:
		return o.Spec.GetResolution() == networkingv1beta1.ServiceEntry_NONE
	default:
		return false
	}
}

func (s Service) Kind() string {
	return s.GroupKind.Kind
}

func (s Service) String() string {
	return serviceKey(s.Kind(), s.GetNamespace(), s.GetName())
}

func (s Service) DefaultVHostName(port ServicePort) string {
	name := "vh_http_" + strconv.Itoa(int(port.Port)) + "_" + s.GetName() + "_" + s.GetNamespace()
	if s.GroupKind.Kind == wellknown.ServiceEntryGVK.Kind {
		// ServiceEntry
		name += "_" + s.Hostname()
	}
	return stringutils.TruncateMaxLength(name, wellknown.EnvoyConfigNameMaxLen)
}

func (s Service) BackendRef(port ServicePort) ir.BackendRefIR {
	backendObj := s.BackendObject(uint32(port.Port)) //nolint:gosec // G115: service port is int32, always in valid range
	return ir.BackendRefIR{
		ClusterName:   backendObj.ClusterName(),
		Weight:        0,
		BackendObject: &backendObj,
		Err:           ErrUnsupportedServiceType,
	}
}

// Destination generated by default for a Waypoint when the
// Service or ServiceEntry has 0 routes attached.
func (s Service) BackendObject(port uint32) ir.BackendObjectIR {
	var hostname string
	if len(s.Hostnames) > 0 {
		// a single ServiceEntry has a Backend/Cluster for every Hostname listed
		// the "default" vhost/route for a ServiceEntry has to pick a single backend to route to
		// so we pick the first hostname.
		// If you DR to s.Hostnames[1] and don't have an HTTPRoute with a Hostname backendRef,
		// you get the backend for Hostnames[0] and the DR doesn't apply (DR sucks in GatewayAPI).
		hostname = s.Hostnames[0]
	} else {
		hostname = fqdn(s.GetName(), s.GetNamespace())
	}

	protocol := ""
	for _, v := range s.Ports {
		if v.Port == int32(port) { //nolint:gosec // G115: port is uint32 representing a port number, safe to convert to int32
			protocol = v.Protocol
			break
		}
	}

	switch obj := s.Object.(type) {
	case *networkingclient.ServiceEntry:
		return serviceentry.BuildServiceEntryBackendObjectIR(
			obj,
			hostname,
			int32(port), //nolint:gosec // G115: port is uint32 representing a port number, safe to convert to int32
			protocol,
			nil, // we just need the cluster name, aliases not important here
		)
	case *corev1.Service:
		return kubernetes.BuildServiceBackendObjectIR(obj, int32(port), protocol) //nolint:gosec // G115: port is uint32 representing a port number, safe to convert to int32
	}

	// fallback: assume k8s
	return ir.BackendObjectIR{
		ObjectSource: ir.ObjectSource{
			Group:     s.GetObjectKind().GroupVersionKind().Group,
			Kind:      s.GetObjectKind().GroupVersionKind().Kind,
			Namespace: s.GetNamespace(),
			Name:      s.GetName(),
		},
		Port:              int32(port), //nolint:gosec // G115: port is uint32 representing a port number, safe to convert to int32
		GvPrefix:          kubernetes.BackendClusterPrefix,
		CanonicalHostname: hostname,
		Obj:               s.Object,
		ObjIr:             nil, // TODO currently
		AttachedPolicies:  ir.AttachedPolicies{},
	}
}

// TODO remove this when we support multiple hostnames
func (s Service) Hostname() string {
	if len(s.Hostnames) == 0 {
		return fqdn(s.GetName(), s.GetNamespace())
	}
	return s.Hostnames[0]
}

// Provider returns the appropriate service provider based on the service type.
// Returns provider.Kubernetes for regular Kubernetes services and provider.External for ServiceEntries.
func (s Service) Provider() provider.ID {
	if s.GroupKind.Kind == wellknown.ServiceEntryGVK.Kind {
		return provider.External
	}
	return provider.Kubernetes
}

// ErrNoServiceVIPs should occur only due to headless Services that have no
// ClusterIP, or ServiceEntry that don't have addresses in Spec or Status.
// We also validate the format of these addresses are either IPs or CIDR ranges,
// but those cases should already get rejected by Kubernetes or Istio validation.
var ErrNoServiceVIPs = errors.New("service has no valid VIPs")

func (svc *Service) CidrRanges() ([]*envoycorev3.CidrRange, error) {
	// TODO support headless by passing dest hostname on TLVs and
	// using that as a filter chain matcher
	cidrRanges := ipsToCidrRanges(svc.Addresses)
	if len(svc.Addresses) == 0 {
		return nil, ErrNoServiceVIPs
	}
	return cidrRanges, nil
}

// ipsToCidrRanges maps a list of strings that can be IPs (1.2.3.4) or cidrs (1.2.3.4/32)
// to CidrRange, picking the correct prefix length for single IPv4 or IPv6 addresses.
func ipsToCidrRanges(ips []string) []*envoycorev3.CidrRange {
	var clusterIPs []*envoycorev3.CidrRange
	for _, addr := range ips {
		cidrRange, err := istioutil.AddrStrToCidrRange(addr)
		if err != nil {
			// this should never happen as either Kubernetes or Istio validation prevents it.
			continue
		}
		clusterIPs = append(clusterIPs, &envoycorev3.CidrRange{
			AddressPrefix: cidrRange.GetAddressPrefix(),
			PrefixLen:     cidrRange.GetPrefixLen(),
		})
	}
	return clusterIPs
}

type ServicePort struct {
	Port       int32
	Protocol   string
	Name       string
	TargetPort int32
}

func (sp ServicePort) IsHTTP() bool {
	// istio supports prefixed protocols like `http-myapp`
	// consider using Istio's kube.ConvertProtocol
	i := strings.IndexByte(sp.Protocol, '-')
	if i >= 0 {
		sp.Protocol = sp.Protocol[:i]
	}

	switch strings.ToLower(sp.Protocol) {
	case "http", "http2", "http_proxy", "grpc", "grpc-web":
		return true
	default:
		return false
	}
}

func fqdn(name, ns string) string {
	clusterDomain := kubeutils.GetClusterDomainName()
	return fmt.Sprintf("%s.%s.svc.%s", name, ns, clusterDomain)
}

func FromService(svc *corev1.Service) Service {
	addrs := serviceAddresses(svc)

	return Service{
		Object:    svc,
		GroupKind: wellknown.ServiceGVK.GroupKind(),
		Addresses: addrs,
		Hostnames: []string{fqdn(svc.GetName(), svc.GetNamespace())},
		Ports: slices.Map(svc.Spec.Ports, func(p corev1.ServicePort) ServicePort {
			protocol := string(p.Protocol)
			if p.AppProtocol != nil {
				protocol = *p.AppProtocol
			} else if p.Name != "" {
				protocol = p.Name
			}
			return ServicePort{
				Port:       int32(p.Port),
				Protocol:   protocol,
				Name:       p.Name,
				TargetPort: int32(p.TargetPort.IntValue()), //nolint:gosec // G115: Kubernetes target port is int, safe to convert to int32
			}
		}),
	}
}

func FromServiceEntry(se *networkingclient.ServiceEntry, aliases []ir.ObjectSource) Service {
	addrs := serviceEntryAddresses(se)

	return Service{
		Object:    se,
		GroupKind: wellknown.ServiceEntryGVK.GroupKind(),
		Aliases:   aliases,
		Addresses: addrs,
		Hostnames: se.Spec.GetHosts(),
		Ports: slices.Map(se.Spec.GetPorts(), func(p *networkingv1beta1.ServicePort) ServicePort {
			return ServicePort{
				Port:       int32(p.Number), //nolint:gosec // G115: ServiceEntry port number is uint32, safe to convert to int32
				Protocol:   string(p.Protocol),
				Name:       p.Name,
				TargetPort: int32(p.TargetPort), //nolint:gosec // G115: ServiceEntry target port is uint32, safe to convert to int32
			}
		}),
	}
}

func BackendAddresses(ir ir.BackendObjectIR) []string {
	var addresses []string
	switch ir.Obj.(type) {
	case *corev1.Service:
		addresses = serviceAddresses(ir.Obj.(*corev1.Service))
	case *networkingclient.ServiceEntry:
		addresses = serviceEntryAddresses(ir.Obj.(*networkingclient.ServiceEntry))
	}
	return addresses
}

// serviceAddresses returns the addresses of the service. ClusterIPs are optional in a Service
// and if exists will include the address of ClusterIP.
// Value can also be "None" (headless service) in both ClusterIPs and ClusterIP.
func serviceAddresses(svc *corev1.Service) []string {
	var addrs []string
	if len(svc.Spec.ClusterIPs) > 0 {
		for _, ip := range svc.Spec.ClusterIPs {
			if ip != "" && ip != "None" {
				addrs = append(addrs, ip)
			}
		}
	}
	if len(addrs) == 0 && len(svc.Spec.ClusterIP) > 0 && svc.Spec.ClusterIP != "None" {
		addrs = []string{svc.Spec.ClusterIP}
	}
	return addrs
}

func serviceEntryAddresses(se *networkingclient.ServiceEntry) []string {
	addrs := append(se.Spec.GetAddresses(), slices.Map(se.Status.GetAddresses(), func(a *networkingv1beta1.ServiceEntryAddress) string {
		return a.Value
	})...)
	return addrs
}

func serviceKey(kind, namespace, name string) string {
	return fmt.Sprintf("%s(%s/%s)", kind, namespace, name)
}

func ServiceKeyFromObject(obj metav1.Object) string {
	var kind string
	switch obj.(type) {
	case *corev1.Service:
		kind = wellknown.ServiceGVK.GroupKind().Kind
	case *networkingclient.ServiceEntry:
		kind = wellknown.ServiceEntryGVK.GroupKind().Kind
	default:
		return ""
	}
	return serviceKey(kind, obj.GetNamespace(), obj.GetName())
}

// Workload is a common type to use between Pod and WorkloadEntry
type Workload struct {
	client.Object
	Addresses []string
	ports     map[string]uint32
}

// PortMapping applied based on the Workload's own rules.
// Only WorkloadEntry will specify this.
func (w Workload) PortMapping(port ServicePort) int32 {
	if w.ports != nil {
		if p, ok := w.ports[port.Name]; ok {
			return int32(p) //nolint:gosec // G115: workload port is uint32, safe to convert to int32
		}
	}
	if port.TargetPort != 0 {
		return port.TargetPort
	}
	return port.Port
}

func FromPod(pod corev1.Pod) Workload {
	var addrs []string
	if len(pod.Status.PodIPs) > 0 {
		addrs = slices.Map(pod.Status.PodIPs, func(pip corev1.PodIP) string {
			return pip.IP
		})
	} else if pod.Status.PodIP != "" {
		addrs = []string{pod.Status.PodIP}
	}
	return Workload{
		Object:    &pod,
		Addresses: addrs,
	}
}

func FromWorkloadEntry(we *networkingclient.WorkloadEntry) Workload {
	var addrs []string
	if len(we.Spec.GetAddress()) > 0 {
		addrs = []string{we.Spec.GetAddress()}
	}
	return Workload{
		Object:    we,
		Addresses: addrs,
		ports:     we.Spec.GetPorts(),
	}
}
